package nes

import (
	"fmt"
	ppu "rgones/nes/ppu/registers"
)

type PPU interface {
	WriteToCtrl(uint8)
	writeToMask(uint8)
	ReadStatus() uint8
	WriteToOamAddr(uint8)
	WriteToOamData(uint8)
	ReadOamData() uint8
	WriteToScroll(uint8)
	WriteToPPUAddr(uint8)
	WriteToData(uint8)
	ReadData() uint8
	WriteOamDMA([256]uint8)
}

type NesPPU struct {
	ChrRom          []byte
	Mirroring       uint8
	Ctrl            ppu.ControlRegister
	Mask            ppu.MaskRegister
	Status          ppu.StatusRegister
	Scroll          ppu.ScrollRegister
	Addr            ppu.AddrRegister
	Vram            [2048]uint8
	OamAddr         uint8
	OamData         [256]uint8
	PaletteTable    [32]uint8
	internalDataBuf uint8
	scanline        uint16
	cycles          uint16
	NmiInterrupt    uint8
	PPU
}

func NewNesPPU(chrRom []uint8, mirroring uint8) *NesPPU {
	nesPPU := &NesPPU{
		ChrRom:          chrRom,
		Mirroring:       mirroring,
		Ctrl:            *ppu.NewControlRegister(),
		Mask:            *ppu.NewMaskRegister(),
		Status:          *ppu.NewStatusRegister(),
		OamAddr:         0,
		Scroll:          *ppu.NewScrollRegister(),
		Addr:            *ppu.NewAddrRegister(),
		Vram:            [2048]uint8{},
		OamData:         [256]uint8{},
		PaletteTable:    [32]uint8{},
		internalDataBuf: 0,
		cycles:          0,
		scanline:        0,
		NmiInterrupt:    255,
	}

	return nesPPU
}

// NewEmptyRom creates a new PPU with an empty CHR ROM
func NewEmptyRom() *NesPPU {
	chrRom := make([]uint8, 0)
	for i := 0; i < 2048; i++ {
		chrRom = append(chrRom, 0)
	}

	nesPPU := NewNesPPU(chrRom, MirroringHorizontal)
	return nesPPU
}

// DumpPPU prints the PPU state
func (ppu *NesPPU) DumpPPU() {
	fmt.Println("--------------------")
	fmt.Printf("PPU\n")
	fmt.Printf("OamAddr: %v\n", ppu.OamAddr)
	fmt.Printf("PaletteTable: %v\n", ppu.PaletteTable)
	fmt.Printf("internalDataBuf: %v\n", ppu.internalDataBuf)
	fmt.Printf("cycles: %v\n", ppu.cycles)
	fmt.Printf("scanline: %v\n", ppu.scanline)
	fmt.Printf("NmiInterrupt: %v\n", ppu.NmiInterrupt)
	fmt.Println("--------------------")
}

// MirrorVramAddr mirrors the VRAM address based on the mirroring mode
func (ppu *NesPPU) MirrorVramAddr(addr uint16) uint16 {
	mirroredVram := addr & 0b10111111111111 // mirror down 0x3000-0x3eff to 0x2000 - 0x2eff
	vramIndex := mirroredVram - 0x2000      // to vram vector
	nameTable := vramIndex / 0x400
	switch {
	case ppu.Mirroring == MirroringVertical && (nameTable == 2 || nameTable == 3):
		return vramIndex - 0x800
	case ppu.Mirroring == MirroringHorizontal && (nameTable == 2 || nameTable == 1):
		return vramIndex - 0x400
	case ppu.Mirroring == MirroringHorizontal && nameTable == 3:
		return vramIndex - 0x800
	default:
		return vramIndex
	}
}

// IncrementVramAddr increments the VRAM address based on the VRAM address increment mode
func (ppu *NesPPU) IncrementVramAddr() {
	ppu.Addr.Increment(ppu.Ctrl.VramAddrIncrement())
}

// Tick increments the PPU cycle and scanline counters
func (ppu *NesPPU) Tick(cycles uint8) bool {
	ppu.cycles += uint16(cycles)
	if ppu.cycles >= 341 {
		ppu.cycles = ppu.cycles - 341
		ppu.scanline++

		if ppu.scanline == 241 {
			ppu.Status.SetVblankStatus(true)
			ppu.Status.SetSpriteZeroHit(false)

			if ppu.Ctrl.GenerateVblankNMI() {
				ppu.NmiInterrupt = 1
			}
		}

		if ppu.scanline >= 262 {
			ppu.scanline = 0
			ppu.NmiInterrupt = 255

			ppu.Status.SetSpriteZeroHit(false)
			ppu.Status.ResetVblankStatus()
			return true
		}
	}
	return false
}

// IsNmiInterruptNone returns true if the NMI interrupt is not set
func (ppu *NesPPU) IsNmiInterruptNone() bool {
	return ppu.NmiInterrupt == 255
}

// PollNmiInterrupt returns the NMI interrupt status and resets it
func (ppu *NesPPU) PollNmiInterrupt() uint8 {
	nmi := ppu.NmiInterrupt
	return nmi
}

// WriteToCtrl writes to the PPU control register
func (ppu *NesPPU) WriteToCtrl(data uint8) {
	beforeNmiStatus := ppu.Ctrl.GenerateVblankNMI()
	ppu.Ctrl.Update(data)
	if !beforeNmiStatus && ppu.Ctrl.GenerateVblankNMI() && ppu.Status.IsInVblank() {
		ppu.NmiInterrupt = 1
	}
}

// WriteToMask writes to the PPU mask register
func (ppu *NesPPU) WriteToMask(data uint8) {
	ppu.Mask.Update(data)
}

// ReadStatus reads the PPU status register
func (ppu *NesPPU) ReadStatus() uint8 {
	data := ppu.Status.Snapshot()
	ppu.Status.ResetVblankStatus()
	ppu.Addr.ResetLatch()
	ppu.Scroll.ResetLatch()
	return data
}

// WriteToOamAddr writes to the OAM address register
func (ppu *NesPPU) WriteToOamAddr(data uint8) {
	ppu.OamAddr = data
}

// WriteToOamData writes to the OAM data register
func (ppu *NesPPU) WriteToOamData(data uint8) {
	ppu.OamData[ppu.OamAddr] = data
	ppu.OamAddr++
}

// ReadOamData reads from the OAM data register
func (ppu *NesPPU) ReadOamData() uint8 {
	return ppu.OamData[ppu.OamAddr]
}

// WriteToScroll writes to the PPU scroll register
func (ppu *NesPPU) WriteToScroll(data uint8) {
	ppu.Scroll.Write(data)
}

// WriteToPPUAddr writes to the PPU address register
func (ppu *NesPPU) WriteToPPUAddr(data uint8) {
	ppu.Addr.Update(data)
}

// WriteToData writes to the PPU data register
func (ppu *NesPPU) WriteToData(data uint8) {
	addr := ppu.Addr.Get()
	switch {
	case addr <= 0x1fff:
		fmt.Printf("attempt to write to chr rom space %v\n", addr)
	case addr >= 0x2000 && addr <= 0x2fff:
		ppu.Vram[ppu.MirrorVramAddr(addr)] = data
	case addr >= 0x3000 && addr <= 0x3eff:
		panic(fmt.Sprintf("addr %v shouldn't be used in reality", addr))
	case addr == 0x3f10 || addr == 0x3f14 || addr == 0x3f18 || addr == 0x3f1c:
		addMirror := addr - 0x10
		ppu.PaletteTable[addMirror-0x3f00] = data
	case addr >= 0x3f00 && addr <= 0x3fff:
		ppu.PaletteTable[addr-0x3f00] = data
	default:
		panic(fmt.Sprintf("unexpected access to mirrored space %v", addr))
	}
	ppu.IncrementVramAddr()

}

// ReadData reads from the PPU data register
func (ppu *NesPPU) ReadData() byte {
	addr := ppu.Addr.Get()

	ppu.IncrementVramAddr()

	println("readAddr", addr)

	switch {
	case addr <= 0x1fff:
		result := ppu.internalDataBuf
		ppu.internalDataBuf = ppu.ChrRom[addr]
		println("0x1fff", ppu.internalDataBuf)
		return result
	case addr >= 0x2000 && addr <= 0x2fff:
		result := ppu.internalDataBuf
		ppu.internalDataBuf = ppu.Vram[ppu.MirrorVramAddr(addr)]
		fmt.Println("0x2000")
		return result
	case addr >= 0x3000 && addr <= 0x3eff:
		panic(fmt.Sprintf("addr %v shouldn't be used in reality", addr))
	case addr == 0x3f10 || addr == 0x3f14 || addr == 0x3f18 || addr == 0x3f1c:
		addMirror := addr - 0x10
		fmt.Println("0x3f10")
		return ppu.PaletteTable[addMirror-0x3f00]
	case addr >= 0x3f00 && addr <= 0x3fff:
		return ppu.PaletteTable[addr-0x3f00]
	default:
		panic(fmt.Sprintf("unexpected access to mirrored space %v", addr))
	}
}

// WriteOamDMA writes to the OAM DMA register
func (ppu *NesPPU) WriteOamDMA(data [256]uint8) {
	for i := 0; i < 256; i++ {
		ppu.OamData[ppu.OamAddr] = data[i]
		ppu.OamAddr++
	}
}
